/*
 *  Cero Project - Copyright   2006 The Cero Developement Team 
 *  (Michael Laguerre, Camille Roux, Matthieu Segret, Mathieu Sivade) 
 * 
 *  This program is free software; you can redistribute it and/or modify it 
 *  under the terms of the GNU General Public License as published by the Free 
 *  Software Foundation; either version 2 of the License, or (at your option) 
 *  any later version.
 * 
 *  This program is distributed in the hope that it will be useful, but 
 *  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
 *  or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 
 *  for more details.
 */

package org.ceroproject.games.base;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.ceroproject.SortableCollection;
import org.ceroproject.SortableList;
import org.ceroproject.games.Action;
import org.ceroproject.games.ActionListener;
import org.ceroproject.games.ActionRule;
import org.ceroproject.games.Game;
import org.ceroproject.games.GameEvent;
import org.ceroproject.games.GameInitializer;
import org.ceroproject.games.GameListener;
import org.ceroproject.games.InitializationException;
import org.ceroproject.games.Player;
import org.ceroproject.games.Round;
import org.ceroproject.games.Rule;
import org.ceroproject.games.StopCondition;
import org.ceroproject.games.ValidationException;


/**
 * This is the main class for any game. You may specialize it at your will, but
 * it can be used as such in basic games. Please note the parameters are
 * provided for ease of use, later when the program is running : you won't have
 * to cast from the base type to your specific type in most cases, this way.
 * 
 * @author Telem
 */
public abstract class GameBase implements Game {
	/** The group of players in the game */
	private SortableList<Player> players = new Players();

	/** The zones common to all players */
	private ZonesBase zones = new ZonesBase();

	/** Collects and dispatches rules to the various rules collections */
	private RuleCollector ruleCollector = new RuleCollector();

	/** The rules used to play the game */
	private SortableList<ActionRule> actionRules = new SortableList<ActionRule>();

	/** The methods used to initialize the game */
	private SortableList<GameInitializer> initializers = new SortableList<GameInitializer>();

	/** The conditions on which, when fulfilled, the game stops */
	private SortableList<StopCondition> gameStopCondition = new SortableList<StopCondition>();

	/** The different kind of rounds to play */
	private List<Round> rounds = new ArrayList<Round>();

	/** The game listeners */
	private Collection<GameListener> gameListeners = new HashSet<GameListener>();

	/** The game listeners */
	private Collection<Action> actions = new ActionCollector();

	private Collection<ActionListener> actionListeners = new HashSet<ActionListener>();

	/** The currently active round */
	private Round currentRound = null;

	private GameRunner gameRunner = null;

	/**
	 * Returns the actions any player can do
	 * 
	 * @return the actions any player can do
	 */
	public Collection<Action> getActions() {
		return actions;
	}

	public SortableCollection<ActionRule> getActionRules() {
		return actionRules;
	}

	public ZonesBase getZones() {
		return zones;
	}

	public List<Round> getRounds() {
		return rounds;
	}

	public SortableList<Player> getPlayers() {
		return players;
	}

	/**
	 * Starts the game. First checks if rules allow the game to be launched,
	 * then initializes the game. Once this is done, a new thread loops through
	 * the rounds again and again until a StopCondition gets checked.
	 * 
	 * @throws InitializationException
	 *             If the initialization screwed
	 * @throws ValidationException
	 *             If one of the rules doesn't validate the game
	 */
	public void startGame() throws InitializationException, ValidationException {
		if (gameRunner != null)
			throw new RuntimeException(
					"There is a game going on already, finish it first !");
		initialize();
		initializeRules();
		validate();

		// starts the game thread
		gameRunner = new GameRunner();
		gameRunner.setDaemon(true);
		gameRunner.start();
	}

	public boolean addGameListener(GameListener listener) {
		return gameListeners.add(listener);
	}

	public boolean removeGameListener(GameListener listener) {
		return gameListeners.remove(listener);
	}

	private void notifyGameStart() {
		for (GameListener list : gameListeners) {
			// using a new event in case the last listener modified it
			GameEvent event = new GameEvent(this);
			list.gameStart(event);
		}
	}

	private void notifyGameEnd() {
		for (GameListener list : gameListeners) {
			// using a new event in case the last listener modified it
			GameEvent event = new GameEvent(this);
			list.gameEnd(event);
		}
	}

	private void initialize() throws InitializationException {
		for (GameInitializer init : initializers) {
			init.initialize(this);
		}
	}

	public void validate() throws ValidationException {
		for (Rule rule : ruleCollector) {
			rule.validateGame(this);
		}

		if (rounds.size() <= 0)
			throw new ValidationException(
					"The game needs to have rounds to be run");
	}

	private void initializeRules() throws InitializationException {
		for (Rule rule : ruleCollector) {
			rule.initializeRule(this);
		}
	}

	/**
	 * Returns whether the game should stop or not
	 * 
	 * @return true if one of the game conditions is fulfilled, false otherwise
	 */
	private boolean checkGameStopConditions() {
		for (StopCondition cond : gameStopCondition) {
			if (cond.shouldStop(this))
				return true;
		}
		return false;
	}

	public void registerAction(Action action) {
		if (currentRound != null) {
			synchronized (currentRound) {
				currentRound.registerAction(action);
				gameRunner.interrupt();
			}
		} else
			throw new RuntimeException("The game is not currently playing !");
	}

	public Collection<Rule> getRules() {
		return ruleCollector;
	}

	public SortableCollection<GameInitializer> getGameInitializers() {
		return initializers;
	}

	public SortableCollection<StopCondition> getGameStopConditions() {
		return gameStopCondition;
	}

	public void dispatchAsListener(Object o) {
		if (o instanceof GameInitializer)
			initializers.add((GameInitializer) o);
		if (o instanceof ActionRule)
			actionRules.add((ActionRule) o);
		if (o instanceof ActionListener)
			actionListeners.add((ActionListener) o);
		if (gameRunner != null && currentRound != null)
			gameRunner.addListeners(currentRound);
	}

	/**
	 * Manages a collection of rules used by the game and dispatches them into
	 * the GameInitializers and ActionRules if necessary
	 * 
	 * @author Telem
	 */
	protected class RuleCollector implements Collection<Rule> {
		private List<Rule> rules = new ArrayList<Rule>();

		public int size() {
			return rules.size();
		}

		public boolean isEmpty() {
			return rules.isEmpty();
		}

		public boolean contains(Object arg0) {
			return rules.contains(arg0);
		}

		public Iterator<Rule> iterator() {
			return rules.iterator();
		}

		public Object[] toArray() {
			return rules.toArray();
		}

		public <T> T[] toArray(T[] arg0) {
			return rules.toArray(arg0);
		}

		public boolean add(Rule rule) {
			if (rules.add(rule)) {
				dispatchAsListener(rule);
				return true;
			} else
				return false;
		}

		public boolean remove(Object rule) {
			if (rules.remove(rule)) {
				if (rule instanceof GameInitializer)
					initializers.remove((GameInitializer) rule);
				if (rule instanceof ActionRule)
					actionRules.remove((ActionRule) rule);
				return true;
			} else
				return false;
		}

		public boolean containsAll(Collection<?> arg0) {
			return rules.containsAll(arg0);
		}

		public boolean addAll(Collection<? extends Rule> arg0) {
			throw new UnsupportedOperationException();
		}

		public boolean removeAll(Collection<?> arg0) {
			throw new UnsupportedOperationException();
		}

		public boolean retainAll(Collection<?> arg0) {
			throw new UnsupportedOperationException();
		}

		public void clear() {
			while (!rules.isEmpty()) {
				this.remove(rules.get(0));
			}
		}
	}

	/**
	 * Collects actions. This represents actions available to all players.
	 * Though it would rarely be used in read mode, it's useful to add actions
	 * to all players at once.
	 * 
	 * @author Telem
	 */
	protected class ActionCollector implements Collection<Action> {
		private Set<Action> actions = new HashSet<Action>();

		public int size() {
			return actions.size();
		}

		public boolean isEmpty() {
			return actions.isEmpty();
		}

		public boolean contains(Object arg0) {
			return actions.contains(arg0);
		}

		public Iterator<Action> iterator() {
			return actions.iterator();
		}

		public Object[] toArray() {
			return actions.toArray();
		}

		public <T> T[] toArray(T[] arg0) {
			return actions.toArray(arg0);
		}

		public boolean add(Action action) {
			if (actions.add(action)) {
				for (Player p : players)
					p.getActions().add(action);
				dispatchAsListener(action);
				return true;
			} else
				return false;
		}

		public boolean remove(Object action) {
			if (actions.remove(action)) {
				for (Player p : players)
					p.getActions().remove(action);
				return true;
			} else
				return false;
		}

		public boolean containsAll(Collection<?> arg0) {
			return actions.containsAll(arg0);
		}

		public boolean addAll(Collection<? extends Action> arg0) {
			throw new UnsupportedOperationException();
		}

		public boolean removeAll(Collection<?> arg0) {
			throw new UnsupportedOperationException();
		}

		public boolean retainAll(Collection<?> arg0) {
			throw new UnsupportedOperationException();
		}

		public void clear() {
			for (Player p : players)
				p.getActions().removeAll(actions);
			actions.clear();
		}
	}

	/**
	 * This Thread will make the game run and allow to have it working on a
	 * separate thread.
	 * 
	 * @author Telem
	 */
	protected class GameRunner extends Thread {
		public GameRunner() {
			super("Game thread");
		}

		public void run() {
			notifyGameStart();
			// we'll loop until the game should stop
			while (!checkGameStopConditions()) {
				// getting the first round
				if (rounds.size() < 1)
					throw new RuntimeException("There is no round to play on !");
				currentRound = rounds.get(0);
				startRound(currentRound);
				// we come back here when the currentRound has ended
				for (int i = 1; i < rounds.size(); i++) {
					// the currentRound cannot be null unless one the rounds'
					// Round was.
					// XXX : it is possible that an action gets registered as
					// the round is changing (though, that'd be stupid from the
					// player). maybe we should add a fix somewhere to prevent
					// this ?
					synchronized (currentRound) {
						currentRound = rounds.get(i);
					}
					startRound(currentRound);
				}
			}
			// the game stops, listeners get notified
			notifyGameEnd();

			// there is no more thread attached to the game
			gameRunner = null;
			currentRound = null;
		}

		public void addListeners(Round r) {
			for (ActionListener actlist : actionListeners)
				r.addActionListener(actlist);
			for (ActionRule actrule : actionRules)
				r.addActionRule(actrule);
		}

		private void startRound(Round r) {
			addListeners(r);
			r.start(GameBase.this);
		}
	}

	public void stopGame() {
		GameStopper stopper = new GameStopper();
		if (currentRound != null) {
			recStop(currentRound, stopper);
			registerAction(stopper);
		}
		this.getGameStopConditions().add(stopper);
	}

	/**
	 * Adds the StopCondition to the round and each of its subround
	 * 
	 * @param round
	 *            The round to add the StopCondition to
	 * @param stopper
	 *            The StopCondition to add to the round
	 */
	private void recStop(Round round, StopCondition stopper) {
		if (round != null) {
			round.addRoundStopCondition(stopper);
			for (Round r : round.getSubRounds()) {
				recStop(r, stopper);
			}
		}
	}

	public boolean addActionListener(ActionListener listener) {
		if (currentRound != null)
			currentRound.addActionListener(listener);
		return actionListeners.add(listener);
	}

	public boolean removeActionListener(ActionListener listener) {
		if (currentRound != null)
			currentRound.removeActionListener(listener);
		return actionListeners.remove(listener);
	}

	private class Players extends SortableList<Player> {
		@Override
		public boolean add(Player player) {
			if (super.add(player)) {
				dispatchAsListener(player);
				return true;
			}
			return false;
		}
	}
}
